// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016-2019, LomboTech Co.Ltd.
 * Authors:
 *	lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute  it and/or modify it
 * under  the terms of  the GNU General  Public License as published by the
 * Free Software Foundation;  either version 2 of the  License, or (at your
 * option) any later version.
 */

#include <drm/drmP.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_encoder.h>
#include <drm/lombo_drm.h>

#include <linux/list.h>
#include <linux/of_device.h>

#include "lombo_crtc.h"
#include "lombo_dc.h"
#include "lombo_drv.h"
#include "lombo_tcon.h"
#include "lombo_wb.h"

enum {
	CRTC_COLOR_ENABLE = 0,
	CRTC_CE_BRIGHTNESS,
	CRTC_CE_SATURATION,
	CRTC_CE_CONTRAST,
	CRTC_CM_SAT_RED,
	CRTC_CM_SAT_ORANGE,
	CRTC_CM_SAT_YELLOW,
	CRTC_CM_SAT_GREEN,
	CRTC_CM_SAT_CYAN,
	CRTC_CM_SAT_BLUE,
	CRTC_CM_SAT_PURPLE,
	CRTC_CM_SAT_MAGENTA,
};

/* handle pageflip events */
void lombo_crtc_handle_vblank_events(struct lombo_crtc *ecrtc)
{
	struct drm_crtc *crtc = &ecrtc->crtc;
	unsigned long flags;

	spin_lock_irqsave(&crtc->dev->event_lock, flags);
	if (ecrtc->event) {
		drm_crtc_send_vblank_event(crtc, ecrtc->event);
		drm_crtc_vblank_put(crtc);
		ecrtc->event = NULL;
	}
	spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
}

static void lombo_crtc_atomic_begin(struct drm_crtc *crtc,
	struct drm_crtc_state *old_crtc_state)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);
	struct drm_device *dev = crtc->dev;
	unsigned long flags;

	DRM_DEBUG_ATOMIC("ecrtc_atomic begin\n");

	lombo_tcon_protect_dbreg(ecrtc->tcon, ecrtc->dc, 1);
	lombo_dc_color_pre_commit(ecrtc->dc);

	if (crtc->state->event) {
		WARN_ON(drm_crtc_vblank_get(crtc) != 0);

		spin_lock_irqsave(&dev->event_lock, flags);
		ecrtc->event = crtc->state->event;
		spin_unlock_irqrestore(&dev->event_lock, flags);

		crtc->state->event = NULL;
	}
}

static panel_flush_cb_t g_panel_flush_cb;

int lombo_crtc_register_panel_flush_cb(panel_flush_cb_t cb)
{
	g_panel_flush_cb = cb;
	return 0;
}

static void lombo_crtc_atomic_flush(struct drm_crtc *crtc,
	struct drm_crtc_state *old_crtc_state)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);
	struct drm_pending_vblank_event *event = crtc->state->event;

	DRM_DEBUG_ATOMIC("ecrtc_atomic flush\n");

	lombo_tcon_protect_dbreg(ecrtc->tcon, ecrtc->dc, 0);

	if (g_panel_flush_cb)
		g_panel_flush_cb();

	if (event) {
		crtc->state->event = NULL;

		spin_lock_irq(&crtc->dev->event_lock);
		if (drm_crtc_vblank_get(crtc) == 0)
			drm_crtc_arm_vblank_event(crtc, event);
		else
			drm_crtc_send_vblank_event(crtc, event);
		spin_unlock_irq(&crtc->dev->event_lock);
	}
}

static void lombo_crtc_atomic_enable(struct drm_crtc *crtc,
	struct drm_crtc_state *old_crtc_state)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	DRM_DEBUG_ATOMIC("ecrtc-atomic enable\n");

	lombo_dc_atomic_enable(ecrtc->dc);
	lombo_tcon_atomic_enable(ecrtc->tcon);
}

static void lombo_crtc_atomic_enable_late(struct drm_crtc *crtc)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	DRM_DEBUG_ATOMIC("ecrtc-atomic enable late\n");

	if (!lombo_disp_boot_sync(crtc))
		lombo_tcon_atomic_start(ecrtc->tcon);
	else
		DRM_DEBUG_ATOMIC("ecrtc-atomic enable late boot sync\n");

	drm_crtc_vblank_on(crtc);
}

static void lombo_crtc_atomic_disable(struct drm_crtc *crtc,
	struct drm_crtc_state *old_crtc_state)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	DRM_DEBUG_ATOMIC("ecrtc-atomic disable\n");

	drm_crtc_vblank_off(crtc);

	lombo_tcon_atomic_disable(ecrtc->tcon);
	lombo_dc_atomic_disable(ecrtc->dc);

	if (crtc->state->event && !crtc->state->active) {
		spin_lock_irq(&crtc->dev->event_lock);
		drm_crtc_send_vblank_event(crtc, crtc->state->event);
		spin_unlock_irq(&crtc->dev->event_lock);

		crtc->state->event = NULL;
	}
}

static void lombo_crtc_mode_set_nofb(struct drm_crtc *crtc)
{
	struct drm_display_mode *mode = &crtc->state->adjusted_mode;
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	DRM_DEBUG_ATOMIC("ecrtc mode set nofb\n");

	lombo_dc_mode_set_nofb(ecrtc->dc, mode);
	lombo_tcon_mode_set_nofb(ecrtc->tcon, mode);
}

static int lombo_crtc_atomic_check(struct drm_crtc *crtc,
	struct drm_crtc_state *state)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);
	uint32_t dc_out_csfmt;
	int ret = 0;

	if (ecrtc->tcon) {
		ret = lombo_tcon_atomic_check(ecrtc->tcon, state);
		if (ret) {
			DRM_ERROR("tcon atomic check fail\n");
			return ret;
		}
		ecrtc->csfmt = lombo_tcon_get_out_csfmt(ecrtc->tcon);
	}
	if (ecrtc->dc) {
		ret = lombo_dc_atomic_check(ecrtc->dc, state);
		if (ret) {
			DRM_ERROR("dc atomic check fail\n");
			return ret;
		}
	}

	/* check and fixup csc */
	dc_out_csfmt = lombo_dc_get_csfmt(ecrtc->dc);
	if (dc_out_csfmt < 0)
		dc_out_csfmt = ecrtc->csfmt;
	DRM_DEBUG_ATOMIC("dc_out_csfmt=%d, tcon_out_csfmt=%d\n",
			 dc_out_csfmt, ecrtc->csfmt);
	if (dc_out_csfmt != ecrtc->csfmt) {
		ecrtc->csfmt = dc_out_csfmt;
		ret = lombo_tcon_set_in_csfmt(ecrtc->tcon, dc_out_csfmt);
		if (ret && ret != -EBUSY)
			DRM_INFO("%s: fixme: ret=%d\n", __func__, ret);
	}

	return 0;
}

static struct drm_crtc_helper_funcs lombo_crtc_helper_funcs = {
	.atomic_check	= lombo_crtc_atomic_check,
	.mode_set_nofb	= lombo_crtc_mode_set_nofb,
	.atomic_begin	= lombo_crtc_atomic_begin,
	.atomic_flush	= lombo_crtc_atomic_flush,
	.atomic_enable	= lombo_crtc_atomic_enable,
	.atomic_enable_late = lombo_crtc_atomic_enable_late,
	.atomic_disable = lombo_crtc_atomic_disable,
};

static int lombo_crtc_enable_vblank(struct drm_crtc *crtc)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	lombo_tcon_enable_vblank(ecrtc->tcon, 1);

	return 0;
}

static void lombo_crtc_disable_vblank(struct drm_crtc *crtc)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	lombo_tcon_enable_vblank(ecrtc->tcon, 0);
}

static int lombo_crtc_atomic_set_property(struct drm_crtc *crtc,
	struct drm_crtc_state *state,
	struct drm_property *property, uint64_t val)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);
	int i;

	for (i = 0; i < ARRAY_SIZE(ecrtc->color.prop); i++)
		if (property == ecrtc->color.prop[i])
			break;
	if (i >= ARRAY_SIZE(ecrtc->color.prop)) {
		DRM_ERROR("unknown prop(%s)\n", property->name);
		return -EINVAL;
	}

	DRM_DEBUG_KMS("prop(%s)=%llu\n", property->name, val);
	ecrtc->color.prop_value[i] = (int)val;
	lombo_dc_set_color_status(ecrtc->dc, i, (int)val);

	return 0;
}

static int lombo_crtc_atomic_get_property(struct drm_crtc *crtc,
	const struct drm_crtc_state *state,
	struct drm_property *property, uint64_t *val)
{
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);
	int i;

	if (property == ecrtc->prop_blk_plane_ids) {
		*val = ecrtc->bl_plane_id->base.id;
		return 0;
	}

	for (i = 0; i < ARRAY_SIZE(ecrtc->color.prop); i++)
		if (property == ecrtc->color.prop[i])
			break;
	if (i >= ARRAY_SIZE(ecrtc->color.prop)) {
		DRM_ERROR("unknown prop(%s)\n", property->name);
		return -EINVAL;
	}
	*val = ecrtc->color.prop_value[i];

	DRM_DEBUG_KMS("prop(%s)=%llu\n", property->name, *val);

	return 0;
}

static const struct drm_crtc_funcs lombo_crtc_funcs = {
	.reset			= drm_atomic_helper_crtc_reset,
	.destroy		= drm_crtc_cleanup,
	.set_config		= drm_atomic_helper_set_config,
	.page_flip		= drm_atomic_helper_page_flip,
	.atomic_duplicate_state	= drm_atomic_helper_crtc_duplicate_state,
	.atomic_destroy_state	= drm_atomic_helper_crtc_destroy_state,
	.enable_vblank		= lombo_crtc_enable_vblank,
	.disable_vblank		= lombo_crtc_disable_vblank,
	.atomic_set_property	= lombo_crtc_atomic_set_property,
	.atomic_get_property	= lombo_crtc_atomic_get_property,
};

static int lombo_crtc_color_properties_init(struct lombo_crtc *ecrtc,
	struct lombo_dc *dc)
{
	int i;
	struct drm_crtc *crtc = &ecrtc->crtc;
	struct drm_device *drm = crtc->dev;
	int *prop_value = ecrtc->color.prop_value;
	static const char * const prop_name[] = {
		"color_enhance_enable",
		"color_enhance_brightness",
		"color_enhance_saturation",
		"color_enhance_contrast",
		"cm_sat_red",
		"cm_sat_orange",
		"cm_sat_yellow",
		"cm_sat_green",
		"cm_sat_cyan",
		"cm_sat_blue",
		"cm_sat_purple",
		"cm_sat_magenta",
	};

	for (i = 0; i < ARRAY_SIZE(prop_name); i++) {
		prop_value[i] = lombo_dc_get_color_status(ecrtc->dc, i);
		if (prop_value[i] < 0)
			continue;
		DRM_DEBUG_DRIVER("creating %s property\n", prop_name[i]);
		ecrtc->color.prop[i] = drm_property_create_range(drm,
				DRM_MODE_PROP_ATOMIC,
				prop_name[i], CRTC_COLOR_MIN_VALUE,
				(i == 0) ? 1 : CRTC_COLOR_MAX_VALUE);
		if (!ecrtc->color.prop[i]) {
			DRM_ERROR("create %s prop fail\n", prop_name[i]);
			return -ENOMEM;
		}
		DRM_DEBUG_DRIVER("created %s property init val[%d]\n",
					prop_name[i], prop_value[i]);
		drm_object_attach_property(&ecrtc->crtc.base,
					ecrtc->color.prop[i], prop_value[i]);
	}

	return 0;
}

static int lombo_crtc_create_properties(struct drm_crtc *crtc)
{
	struct drm_device *drm = crtc->dev;
	struct lombo_crtc *ecrtc = to_lombo_crtc(crtc);

	ecrtc->prop_blk_plane_ids = drm_property_create(drm,
		DRM_MODE_PROP_BLOB, "blk_plane_ids", 0);
	if (!ecrtc->prop_blk_plane_ids) {
		DRM_ERROR("create blk_plane_ids prop fail\n");
		return -ENOMEM;
	}

	return 0;
}

static int lombo_crtc_properties_init(struct lombo_crtc *ecrtc,
	struct lombo_dc *dc)
{
	struct drm_crtc *crtc = &ecrtc->crtc;
	int ret;

	ret = lombo_crtc_create_properties(crtc);
	if (ret) {
		DRM_ERROR("create the crtc properties fail, ret=%d\n", ret);
		return ret;
	}

	if (ecrtc->bl_plane_id) {
		drm_object_attach_property(&crtc->base,
					   ecrtc->prop_blk_plane_ids,
					   ecrtc->bl_plane_id->base.id);
		DRM_DEBUG("attach blk plane id\n");
	}

	return ret;
}

struct lombo_crtc *lombo_crtc_init(struct drm_device *drm,
	struct lombo_dc *dc, struct lombo_tcon *tcon)
{
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_boot_disp *bd = drv->boot_disp;
	struct lombo_crtc *ecrtc;
	struct drm_plane **planes;
	struct drm_plane *primary = NULL, *cursor = NULL;
	int i, ret;

	ecrtc = devm_kzalloc(drm->dev, sizeof(*ecrtc), GFP_KERNEL);
	if (!ecrtc)
		return ERR_PTR(-ENOMEM);

	ecrtc->dc = dc;
	ecrtc->tcon = tcon;
	lombo_dc_set_crtc_tcon(dc, ecrtc, tcon);

	/* create our planes by dc */
	planes = lombo_dc_planes_init(dc);
	if (IS_ERR(planes)) {
		DRM_DEV_ERROR(drm->dev, "Couldn't create the planes\n");
		return (void *)planes;
	}

	/* find primary and cursor planes for drm_crtc_init_with_planes */
	for (i = 0; planes[i]; i++) {
		struct drm_plane *plane = planes[i];

		switch (plane->type) {
		case DRM_PLANE_TYPE_PRIMARY:
			primary = plane;
			break;
		case DRM_PLANE_TYPE_CURSOR:
			cursor = plane;
			break;
		default:
			break;
		}
	}

	if (primary)
		ret = drm_crtc_init_with_planes(drm, &ecrtc->crtc,
						primary, cursor,
						&lombo_crtc_funcs, NULL);
	else
		ret = drm_crtc_init(drm, &ecrtc->crtc, &lombo_crtc_funcs);
	if (ret) {
		DRM_DEV_ERROR(drm->dev, "Couldn't init DRM CRTC\n");
		return ERR_PTR(ret);
	}

	drm_crtc_helper_add(&ecrtc->crtc, &lombo_crtc_helper_funcs);

	lombo_crtc_color_properties_init(ecrtc, dc);
	lombo_crtc_properties_init(ecrtc, dc);

	/* Set possible_crtcs to this crtc for overlay planes */
	for (i = 0; planes[i]; i++) {
		uint32_t possible_crtcs = drm_crtc_mask(&ecrtc->crtc);
		struct drm_plane *plane = planes[i];

		if (plane->type == DRM_PLANE_TYPE_OVERLAY)
			plane->possible_crtcs = possible_crtcs;
	}

	/* parse crtc_id of boot_disp */
	if (bd && (bd->tcon_id == lombo_tcon_id(tcon))) {
		bd->crtc = &ecrtc->crtc;
		DRM_DEBUG("bd tcon_id=%d, crtc_id=%d\n",
			bd->tcon_id, ecrtc->crtc.index);
	}

	return ecrtc;
}

int lombo_crtc_kms_show(struct seq_file *m, void *data)
{
	struct drm_info_node *node = m->private;
	struct drm_device *drm = node->minor->dev;
	unsigned int index;

	for (index = 0;; index++) {
		struct drm_crtc *crtc = drm_crtc_from_index(drm, index);
		struct lombo_crtc *ecrtc;
		struct lombo_dc *dc;
		struct lombo_tcon *tcon;

		if (!crtc)
			break;
		ecrtc = to_lombo_crtc(crtc);
		dc = ecrtc->dc;
		tcon = ecrtc->tcon;

		seq_printf(m, "PIPE(%d): CRTC(%d), TCON(%d)\n",
			drm_crtc_index(crtc), crtc->base.id,
			lombo_tcon_id(tcon));
		lombo_tcon_kms_show(tcon, m, data);
		lombo_dc_kms_show(dc, m, data);
	}
	return 0;
}

/* lombo wb ioctl wrapper */

int lombo_wb_set_para_ioctl(struct drm_device *drm,
	void *data, struct drm_file *file)
{
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_wb *wb = drv->wb;
	struct lombo_wb_funcs *funcs = drv->wb_funcs;

	if (funcs && wb)
		return funcs->set_para(wb, data);
	else
		return -ENODEV;
}

int lombo_wb_queue_buf_ioctl(struct drm_device *drm,
	void *data, struct drm_file *file)
{
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_wb *wb = drv->wb;
	struct lombo_wb_funcs *funcs = drv->wb_funcs;

	if (funcs && wb) {
		struct drm_lombo_wb_queue_buf *qbuf = data;
		struct lombo_wb_que_buf que_buf;
		int ret;

		que_buf.fb = drm_framebuffer_lookup(drm, file, qbuf->fb);
		if (!que_buf.fb) {
			DRM_ERROR("lookup fb %u fail\n", qbuf->fb);
			return -EINVAL;
		}
		que_buf.release_fence = qbuf->release_fence;
		que_buf.user_priv = qbuf->user_priv;
		ret = funcs->queue_buf(wb, &que_buf);
		drm_framebuffer_unreference(que_buf.fb);

		return ret;
	} else {
		return -ENODEV;
	}
}

int lombo_wb_dequeue_buf_ioctl(struct drm_device *drm,
	void *data, struct drm_file *file)
{
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_wb *wb = drv->wb;
	struct lombo_wb_funcs *funcs = drv->wb_funcs;

	if (funcs && wb)
		return funcs->dequeue_buf(wb, data);
	else
		return -ENODEV;
}

int lombo_wb_start_ioctl(struct drm_device *drm,
	void *data, struct drm_file *file)
{
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_wb *wb = drv->wb;
	struct lombo_wb_funcs *funcs = drv->wb_funcs;

	if (funcs && wb)
		return funcs->start(wb, data);
	else
		return -ENODEV;
}

int lombo_wb_stop_ioctl(struct drm_device *drm,
	void *data, struct drm_file *file)
{
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_wb *wb = drv->wb;
	struct lombo_wb_funcs *funcs = drv->wb_funcs;

	if (funcs && wb)
		return funcs->stop(wb);
	else
		return -ENODEV;
}

/* lombo drm util apis */

int lombo_dev_get_id(struct device *dev, const char *s)
{
	const struct of_device_id *match;
	const char *str;
	int id, ret;

	match = of_match_device(dev->driver->of_match_table, dev);
	if (!s || !match)
		return 0;

	str = strstr(match->compatible, s);
	if (!str)
		return 0;
	str += strlen(s);
	ret = kstrtoint(str, 10, &id);
	DRM_DEV_DEBUG(dev, "ret=%d, str=%s, id=%d\n", ret, str, id);
	if (ret)
		return 0;

	return id;
}

int lombo_fb_kmap(uint64_t phy_addr, uint32_t size, int cache,
	void **map_vaddr, uint32_t *map_offset)
{
	uint64_t addr;
	uint32_t offset;
	void *vaddr;

	int npages;
	struct page **pages, **p, *cur_page;
	pgprot_t pgprot;
	int i;

	offset = (uint32_t)(phy_addr & (PAGE_SIZE - 1));
	addr = phy_addr - offset;
	DRM_DEBUG("offset=0x%x, addr=0x%llx\n", offset, addr);

	size += offset;
	npages = PAGE_ALIGN(size) / PAGE_SIZE;
	pages = vmalloc(npages * sizeof(*pages));
	if (!pages) {
		DRM_ERROR("alloc for %d pages fail\n", npages);
		return -ENOMEM;
	}

	p = pages;
	cur_page = phys_to_page(addr);
	for (i = 0; i < npages; i++)
		*(p++) = cur_page++;

	if (cache)
		pgprot = PAGE_KERNEL;
	else
		pgprot = pgprot_noncached(PAGE_KERNEL);

	vaddr = vmap(pages, npages, VM_MAP, pgprot);
	vfree(pages);

	if (!vaddr) {
		DRM_ERROR("vmap fail\n");
		return -EINVAL;
	}
	*map_vaddr = vaddr;
	*map_offset = offset;

	return 0;
}

void lombo_fb_kunmap(void **map_vaddr)
{
	if (map_vaddr && *map_vaddr) {
		vunmap(*map_vaddr);
		*map_vaddr = NULL;
	}
}

int lombo_strtou64(const char *s, char **end, uint64_t *res, uint32_t base)
{
	char val[32];
	int i = 0;
	int ret;
	uint64_t rv;

	while ((-1 != hex_to_bin(*s)) && (i < sizeof(val) - 1)) {
		val[i] = *s;
		i++;
		s++;
	}
	if (i == sizeof(val) - 1) {
		DRM_DEBUG("val=%s, i=%d\n", val, i);
		return -EINVAL;
	}
	val[i] = '\0';
	ret = kstrtou64(val, base, &rv);
	if (ret) {
		DRM_DEBUG("val=%s, ret=%d\n", val, ret);
		return ret;
	}

	*end = (char *)s;
	*res = rv;
	return 0;
}

int lombo_strtou32(const char *s, char **end, uint32_t *res, uint32_t base)
{
	uint64_t tmp;
	int ret;

	ret = lombo_strtou64(s, end, &tmp, base);
	if (ret)
		return ret;

	if (tmp != (uint64_t)(uint32_t)tmp)
		return -ERANGE;

	*res = (uint32_t)tmp;
	return 0;
}

